package Alibaba.AlibabaServer;

import java.beans.*;

import java.io.*;
import java.net.*;
import java.util.*;

import Alibaba.AlibabaUtil.*;
import Alibaba.AlibabaUtil.Base64;

import com.sun.net.httpserver.*;

public class AlibabaHttpServer {

	protected static void printMainHelp() {
		System.out.println("Usage: AlibabaServer <backup directory>");
	}

	public static void main(String[] args) throws IOException {
		InetSocketAddress addr = new InetSocketAddress(8080);
		HttpServer server = HttpServer.create(addr, 0);

		if (args.length < 1) {
			printMainHelp();
			return;
		}

		String backupDir = args[0];
		if (!backupDir.endsWith("\\") && !backupDir.endsWith("/")) {
			System.out.println("Failed: " + backupDir + "isn't a valid directory name");
			return;
		}

		server.createContext("/", new MyHandler(backupDir));
		server.start();
		System.out.println("Server is listening on port 8080. Backup directory: " + backupDir);
	}
}

class MyHandler implements HttpHandler {
	private String backupDir = "";
	private File listingDat;
	Listing listing;

	public MyHandler(String backupDir) throws IOException {
		this.backupDir = backupDir;

		listingDat = new File(backupDir + "listing.dat");
		if (listingDat.exists()) {
			XMLDecoder xmlDecoder = new XMLDecoder(new BufferedInputStream(new FileInputStream(listingDat)));
			listing = (Listing) xmlDecoder.readObject();
			/* consistency check */
			System.out.println("Running consistency check...");
			List<ListingEntry> allFileEntries = listing.getEntries();
			for (ListingEntry entry : allFileEntries) {
				byte[] shaBinListing = entry.getFileHash();
				byte[] fileBuffer = readLocalFileName(entry.getLocalName());
				if (fileBuffer != null) {
					byte[] shaBin = SHACoder.calcSHA256(fileBuffer, fileBuffer.length);
					if (!Arrays.equals(shaBin, shaBinListing)) {
						System.out.println("Entry " + entry.getFilename() + "(Local: " + entry.getLocalName() + ") of user " + entry.getOwner()
								+ " doesn't match server's listing.");
						Arrays.fill(shaBinListing, (byte) 0);
					}
				} else {
					System.out.println("Entry " + entry.getFilename() + "(Local: " + entry.getLocalName() + ") of user " + entry.getOwner()
							+ " doesn't match server's listing.");
					Arrays.fill(shaBinListing, (byte) 0);
				}
			}
			System.out.println("Done running consistency check.");
		} else {
			listing = new Listing();
		}

	}

	private String combine(String[] s, String glue) {
		int k = s.length;
		if (k == 0) {
			return "";
		}
		StringBuilder out = new StringBuilder();
		out.append(s[0]);
		for (int x = 1; x < k; ++x) {
			out.append(glue).append(s[x]);
		}
		out.append(glue);
		return out.toString();
	}

	public void handle(HttpExchange exchange) throws IOException {

		String requestMethod = exchange.getRequestMethod();
		String userName = exchange.getRequestHeaders().getFirst("Authorization");
		Headers responseHeaders = exchange.getResponseHeaders();
		responseHeaders.set("Content-Type", "text/plain");

		if (userName != null) {
			System.out.println("The user name is: " + userName);
		} else {
			System.out.println("No username provided by client.");
			exchange.sendResponseHeaders(HttpURLConnection.HTTP_BAD_REQUEST, 0);
			System.out.println("Sent HTTP_BAD_REQUEST response.");
			return;
		}

		URI url = exchange.getRequestURI();
		String uriPath = url.getPath();

		int snapshotNum = 0;
		if (url.getQuery() != null && url.getQuery().startsWith("snapshot=")) {
			String querySnapshot = getQueryParameter(url.getQuery());
			if (querySnapshot != null) {
				snapshotNum = parseSnapshot(querySnapshot);
			}
		}
		System.out.println("The snapshot number is " + snapshotNum);
		if (requestMethod.equalsIgnoreCase("GET")) {
			System.out.println("GET " + url);

			if (uriPath.endsWith("/")) {// (GET) uri is a directory: list files & check files

				String responseString = null;

				if (url.getQuery() != null && url.getQuery().equals("check")) { // (GET) check files request
					/* taking all the files names in requested directory */
					List<ListingEntry> allFileEntries = listing.getEntriesByPrefix(userName, -1, uriPath, false);

					System.out.println("Check files request for " + uriPath);
					HashMap<String, List<Integer>> hash = new HashMap<String, List<Integer>>();

					for (ListingEntry entry : allFileEntries) {
						byte[] fileBuffer = readLocalFileName(entry.getLocalName());
						byte[] shaBin = null;
						if (fileBuffer != null) {
							shaBin = SHACoder.calcSHA256(fileBuffer, fileBuffer.length);
							if (!Arrays.equals(shaBin, entry.getFileHash())) {

								Arrays.fill(shaBin, (byte) 0);
							}
						} else {
							shaBin = entry.getFileHash();
							Arrays.fill(shaBin, (byte) 0);
						}
						String shaBase64 = Base64.encodeBytes(shaBin, Base64.URL_SAFE);

						String hashKey = entry.getFilename() + '@' + shaBase64;
						if (hash.get(hashKey) == null) {
							hash.put(hashKey, new ArrayList<Integer>());
						}
						hash.get(hashKey).add(entry.getSnapshotVersion()[0]);
					}

					int index = 0;
					String[] fileNamesAndSignature = new String[hash.keySet().size()];
					for (String key : hash.keySet()) {
						String[] splitedKey = key.split("@");
						String fileName = splitedKey[0];
						String sha = splitedKey[1];

						fileNamesAndSignature[index] = fileName + "," + listToString(hash.get(key)) + "," + sha;
						index++;
					}

					responseString = combine(fileNamesAndSignature, "\n");
				}

				else { // list file request
					System.out.println("List files request for " + uriPath);

					if (!listing.isSnapshotExists(userName, snapshotNum)) {
						exchange.sendResponseHeaders(HttpURLConnection.HTTP_INTERNAL_ERROR, 0);
						System.out.println("Sent HTTP_INTERNAL_ERROR response.");
						return;
					}

					/* taking all the files names in requested directory */
					List<ListingEntry> allFileEntries = listing.getEntriesByPrefix(userName, snapshotNum, uriPath, false);

					String[] fileNames = new String[allFileEntries.size()];
					int index = 0;
					for (ListingEntry entry : allFileEntries) {
						fileNames[index] = entry.getFilename();
						index++;
					}

					responseString = combine(fileNames, "\n");
				}

				exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
				System.out.println("Sent HTTP_OK response.");
				OutputStream responseStream = exchange.getResponseBody();
				responseStream.write(responseString.getBytes());
				responseStream.close();
			}

			else { // (GET) uri is a file: restore file request
				System.out.println("Restore file request for " + uriPath);
				ListingEntry listEntry = listing.getListing(userName, snapshotNum, uriPath);
				if (listEntry == null) {
					exchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, 0);
					System.out.println("Sent HTTP_NOT_FOUND response.");
					return;
				}
				String localName = listEntry.getLocalName();
				byte[] fileBuffer = readLocalFileName(localName);
				if (fileBuffer == null) {
					exchange.sendResponseHeaders(HttpURLConnection.HTTP_INTERNAL_ERROR, 0);
					System.out.println("Sent HTTP_INTERNAL_ERROR response.");
					return;
				}

				exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
				System.out.println("Sent HTTP_OK response.");
				OutputStream responseStream = exchange.getResponseBody();

				if (url.getQuery() != null && url.getQuery().equals("signature")) {

					byte[] sigBin = SHACoder.calcSHA256(fileBuffer, fileBuffer.length);
					String sigBase64 = Base64.encodeBytes(sigBin, Base64.URL_SAFE);
					sigBase64 += "\n";
					responseStream.write(sigBase64.getBytes());
				} else {
					responseStream.write(fileBuffer);
				}

				responseStream.close();
			}
		}

		if (requestMethod.equalsIgnoreCase("PUT")) { // (PUT) backup file request
			byte[] newFileSig;
			String localName;

			System.out.println("PUT " + url);

			if (url.getQuery() != null) {
				if (url.getQuery().startsWith("signature=")) { // Backup signature
					System.out.println("Backup file by signature request for " + uriPath);

					String querySignature = getQueryParameter(url.getQuery());
					if (querySignature == null) {
						exchange.sendResponseHeaders(HttpURLConnection.HTTP_INTERNAL_ERROR, 0);
						System.out.println("Sent HTTP_INTERNAL_ERROR response.");
						return;
					}

					String sigBase64 = querySignature;
					byte[] sig = Base64.decode(sigBase64, Base64.URL_SAFE);
					localName = null;
					for (ListingEntry entry : listing.getEntries()) {
						if (Arrays.equals(entry.getFileHash(), sig)) {
							localName = entry.getLocalName();
							break;
						}
					}

					if (localName == null) {
						exchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, 0);
						System.out.println("Sent HTTP_NOT_FOUND response.");
						return;
					}

					newFileSig = sig;
				} else { // create snapshot command
					System.out.println("Create snapshot command");

					int newSnapshotNum = parseSnapshot(url.getQuery());
					// Check that the input snapshot is valid, that the snapshot number isn't in used, and that the user has files
					if (newSnapshotNum == 0 || listing.isSnapshotExists(userName, newSnapshotNum) || !listing.isSnapshotExists(userName, 0)) {
						exchange.sendResponseHeaders(HttpURLConnection.HTTP_INTERNAL_ERROR, 0);
						System.out.println("Sent HTTP_INTERNAL_ERROR response.");
						return;
					}

					for (ListingEntry entry : listing.getEntriesByPrefix(userName, 0, "/", true)) {
						listing.addListing(userName, newSnapshotNum, entry.getFilename(), entry.getLocalName(), entry.getFileHash());
					}

					XMLEncoder xmlEncoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream(listingDat)));
					xmlEncoder.writeObject(listing);
					xmlEncoder.close();

					exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
					System.out.println("Sent HTTP_OK response.");
					return;
				}

			} else { // Backup by data
				// Get requested filename
				localName = convertPathToName(userName, 0, uriPath);

				// if more than one user use the file or this is anew file
				if (localName == null || getNumOfPointers(localName) > 1) {
					localName = getNewFileName();
				}

				String requestedFileName = backupDir + localName;
				File requestedFile = new File(requestedFileName);

				System.out.println("Backup file by data request by data for " + uriPath);

				// Saving all attached file data and save file to bytes buffer
				InputStream requestBody = exchange.getRequestBody();
				int savedFileSize = Integer.parseInt(exchange.getRequestHeaders().getFirst("Content-Length"));
				byte[] requestedFileBuffer = new byte[savedFileSize];
				requestedFileBuffer = readInputStream(requestBody);
				if (requestedFileBuffer.length < savedFileSize) {// We were not able to read the entire file
					System.out.println("Sent HTTP_INTERNAL_ERROR response.");
					exchange.sendResponseHeaders(HttpURLConnection.HTTP_INTERNAL_ERROR, 0);
					return;
				}

				try {
					OutputStream outputFile = new FileOutputStream(requestedFile);
					outputFile.write(requestedFileBuffer);
					outputFile.close();
				} catch (Exception e) {
					exchange.sendResponseHeaders(HttpURLConnection.HTTP_INTERNAL_ERROR, 0);
					System.out.println("Sent HTTP_INTERNAL_ERROR response.");
					return;
				}

				newFileSig = SHACoder.calcSHA256(requestedFileBuffer, requestedFileBuffer.length);
			}

			// Update server's listing
			listing.addListing(userName, 0, uriPath, localName, newFileSig);
			XMLEncoder xmlEncoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream(listingDat)));
			xmlEncoder.writeObject(listing);
			xmlEncoder.close();

			exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
			System.out.println("Sent HTTP_OK response.");
		}

		if (requestMethod.equalsIgnoreCase("DELETE")) { // (DELETE) delete snapshot request

			System.out.println("DELETE " + url);

			System.out.println("Delete snapshot command");

			int delSnapshotNum = parseSnapshot(url.getQuery());
			// Check that the input snapshot is valid and that the snapshot number exists
			if (delSnapshotNum == 0 || !listing.isSnapshotExists(userName, delSnapshotNum)) {
				exchange.sendResponseHeaders(HttpURLConnection.HTTP_INTERNAL_ERROR, 0);
				System.out.println("Sent HTTP_INTERNAL_ERROR response.");
				return;
			}

			listing.removeKeys(userName, delSnapshotNum);

			XMLEncoder xmlEncoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream(listingDat)));
			xmlEncoder.writeObject(listing);
			xmlEncoder.close();

			exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
			System.out.println("Sent HTTP_OK response.");
			return;
		}

	}

	private byte[] readLocalFileName(String localName) throws IOException {
		String requestedFileName = backupDir + localName;

		File inputFile = new File(requestedFileName);
		if (!inputFile.canRead()) {
			return null;
		} else {
			byte[] bytes = new byte[(int) inputFile.length()];
			FileInputStream inputStream = new FileInputStream(inputFile);
			// note that this implementation limits the maximal files size

			int streamSize = inputStream.read(bytes);
			if (streamSize < bytes.length) {
				inputStream.close();
				return null;
			}
			inputStream.close();
			return bytes;
		}
	}

	static byte[] readInputStream(InputStream stream) throws IOException {
		final int chunkSize = 2048;
		byte[] buf = new byte[chunkSize];
		ByteArrayOutputStream byteStream = new ByteArrayOutputStream(chunkSize);
		int count;
		while ((count = stream.read(buf)) != -1)
			byteStream.write(buf, 0, count);
		return byteStream.toByteArray();
	}

	/* Return the local name of the file as is store in the server returns null if not exist */
	String convertPathToName(String userName, int snapshotNum, String path) {
		return listing.getListing(userName, snapshotNum, path) != null ? listing.getListing(userName, snapshotNum, path).getLocalName() : null;
	}

	String getNewFileName() {
		return "" + listing.getFileCounter();
	}

	/* Return the num of pointers from listing to specific local file name */
	int getNumOfPointers(String localName) {
		int counter = 0;
		for (ListingEntry entry : listing.getEntries()) {
			if (entry.getLocalName().equals(localName)) {
				counter++;
			}
		}
		return counter;
	}

	private String getQueryParameter(String query) {
		int index = query.indexOf('=');
		if (index == -1 || index + 1 >= query.length()) {
			return null;
		}
		return query.substring(index + 1);
	}

	private int parseSnapshot(String querySnapshot) {
		int snapshotNum = 0;
		try {
			snapshotNum = Integer.parseInt(querySnapshot);
		} catch (NumberFormatException e) {
			return 0;
		}
		if (!listing.isValidSnapshot(snapshotNum)) {
			return 0;
		}
		return snapshotNum;
	}

	private String listToString(List<Integer> list) {
		String result = "";
		for (Integer num : list) {
			result += num + " ";
		}
		return result.trim();
	}

}
